Skip to content

fix(websocket): don't panic on a plain HTTP request to a WebSocket route - #3932

Open
om7057 wants to merge 4 commits into
gofr-dev:developmentfrom
om7057:fix/websocket-plain-http-panic
Open

fix(websocket): don't panic on a plain HTTP request to a WebSocket route#3932
om7057 wants to merge 4 commits into
gofr-dev:developmentfrom
om7057:fix/websocket-plain-http-panic

Conversation

@om7057

@om7057 om7057 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #3862.

App.WebSocket registered its handler with an unsafe type assertion on WSConnectionKey. A plain HTTP GET to a WebSocket route (no Connection: Upgrade / Upgrade: websocket headers) never gets that key set on the request context by the upgrade middleware, so the assertion ctx.Request.Context().Value(websocket.WSConnectionKey).(string) panicked with interface conversion: interface {} is nil, not string.

GetWebsocketConnection also returns a nil *Connection for an unknown connection ID, so even a safe type assertion would still hit a nil pointer dereference at the following conn.Conn == nil check.

Both call sites now check ok/nil before dereferencing and return the existing websocket.ErrorConnection instead of panicking, matching the fix direction suggested in the issue.

Test plan

  • go build ./...
  • go vet ./...
  • go test ./pkg/gofr/...
  • New test: Test_WebSocket_PlainHTTPRequestDoesNotPanic sends a plain GET to a route registered via app.WebSocket and asserts the response carries websocket.ErrorConnection's message rather than the generic panic-recovery message (both map to HTTP 500, so the response body is the only way to distinguish "handled cleanly" from "panicked and got recovered"). Verified this test fails with the exact panic from the issue when the fix is reverted, and passes with it applied.

@om7057

om7057 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

@Umang01-hash @coolwednesday a small reminder on this!

@aryanmehrotra aryanmehrotra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the wait, and for missing your ping — thirteen days on a remotely-triggerable panic is too
long.

Bug is real, fix is right. Ran both builds in containers:

development   500  Internal Server Error   + panic: interface conversion: interface {} is nil, not string
this branch   500  couldn't establish connection to web socket   + no panics

Both guards are needed — GetWebsocketConnection does return a nil *Connection for an unknown ID.

One change: the status code on the first return site.

The issue asked for 4xx and this keeps the pre-existing 500. Not a regression, but RFC 6455 §4.2.1
says a failed handshake gets "an appropriate error code (such as 400 Bad Request)" — and we already
do that twelve lines away, in middleware/web_socket.go:21, when an upgrade is attempted and fails.
A request that never attempted one should not be treated as more serious than one that tried.

Only the first site though — the two are different failures:

if !ok {
    return nil, websocket.ErrorConnection   // plain GET, no upgrade -> client error, 400
}
if conn == nil || conn.Conn == nil {
    return nil, websocket.ErrorConnection   // upgrade ran, connection missing -> server fault, keep 500
}

So please return a distinct error at the first site rather than putting StatusCode() on the shared
ErrorConnection — folding both into a 4xx would hide a genuine bug class. ErrorConnection has no
other references in the repo and only your new test pins a status, so it is a one-line assertion
change.

(426 looks tempting but is wrong here — RFC 6455 reserves it for version negotiation in §4.2.2.)

Also: this branch predates #4051 so it does not build standalone against current development.
Merges cleanly — I tested the merged result — just worth a rebase.

om7057 added 4 commits August 27, 2026 14:24
Addresses gofr-dev#3862. App.WebSocket registered its handler with an
unsafe type assertion on WSConnectionKey: a plain GET with no Upgrade
headers never gets that key set on the request context by
WSHandlerUpgrade, so the assertion panicked with "interface conversion:
interface {} is nil, not string". GetWebsocketConnection also returns a
nil *Connection for an unknown ID, so the subsequent conn.Conn == nil
check was itself a nil pointer dereference once reached.

Both call sites now check ok/nil before dereferencing and return the
existing websocket.ErrorConnection instead of panicking.
Use http.NewRequestWithContext + http.DefaultClient.Do instead of the
context-less http.Get.
…cket upgrade

Addresses review feedback on gofr-dev#3932: the "no WSConnectionKey on context"
case (a plain HTTP request that never went through the upgrade handshake)
is a client error, distinct from the "upgrade ran but the connection is
missing" case, which is a server-side fault. Splitting them keeps the
existing 500 for the latter and adds a new websocket.ErrorNotWebSocketUpgrade
type for the former, mapping to 400 per RFC 6455 4.2.1, consistent with the
400 middleware/web_socket.go already returns when an upgrade attempt fails.
@om7057
om7057 force-pushed the fix/websocket-plain-http-panic branch from 09bd23a to ce48537 Compare August 27, 2026 08:58
@om7057

om7057 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Sorry for the wait, and for missing your ping — thirteen days on a remotely-triggerable panic is too long.

Bug is real, fix is right. Ran both builds in containers:

development   500  Internal Server Error   + panic: interface conversion: interface {} is nil, not string
this branch   500  couldn't establish connection to web socket   + no panics

Both guards are needed — GetWebsocketConnection does return a nil *Connection for an unknown ID.

One change: the status code on the first return site.

The issue asked for 4xx and this keeps the pre-existing 500. Not a regression, but RFC 6455 §4.2.1 says a failed handshake gets "an appropriate error code (such as 400 Bad Request)" — and we already do that twelve lines away, in middleware/web_socket.go:21, when an upgrade is attempted and fails. A request that never attempted one should not be treated as more serious than one that tried.

Only the first site though — the two are different failures:

if !ok {
    return nil, websocket.ErrorConnection   // plain GET, no upgrade -> client error, 400
}
if conn == nil || conn.Conn == nil {
    return nil, websocket.ErrorConnection   // upgrade ran, connection missing -> server fault, keep 500
}

So please return a distinct error at the first site rather than putting StatusCode() on the shared ErrorConnection — folding both into a 4xx would hide a genuine bug class. ErrorConnection has no other references in the repo and only your new test pins a status, so it is a one-line assertion change.

(426 looks tempting but is wrong here — RFC 6455 reserves it for version negotiation in §4.2.2.)

Also: this branch predates #4051 so it does not build standalone against current development. Merges cleanly — I tested the merged result — just worth a rebase.

Sure, no worries @aryanmehrotra.
Split the two error cases as suggested: added websocket.ErrorNotWebSocketUpgrade{} with StatusCode() int returning 400 for the "never attempted an upgrade" path, kept ErrorConnection as-is (falls through to 500) for the "upgrade ran, connection missing" path. Updated the test to assert the 400 and the new message.

Also rebased onto current development, builds standalone now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: plain HTTP request to a WebSocket route panics (unchecked type assertion + nil deref)

2 participants